Refactor: host build graph, eager completion watermark, 4 AICPU schedulers - #1618
Refactor: host build graph, eager completion watermark, 4 AICPU schedulers#1618raphael-s-steiner wants to merge 1 commit into
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR removes the dedicated resolution thread from the AICPU scheduler, unifying all threads under a single scheduling model. Completion flags change from byte-based to int32 identity stamps with stricter watermark semantics, requiring updates across shared memory, orchestrator, runtime, and scheduler code, plus a per-thread retry mechanism, documentation, and a new test. ChangesScheduler unification and completion-flag rework
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant SchedulerThread
participant CompletionFlags
participant WakeList
participant Watermark
SchedulerThread->>CompletionFlags: try_set_completion_flag(thread_idx, local_id)
alt flag set successfully
CompletionFlags-->>SchedulerThread: success
SchedulerThread->>WakeList: drain_wake_list(thread_idx)
SchedulerThread->>Watermark: update_completed_watermark(thread_idx, local_id)
else reuse not yet certified
CompletionFlags-->>SchedulerThread: failure
SchedulerThread->>SchedulerThread: push to failed_heap_of_set_completion_flag
SchedulerThread->>SchedulerThread: retry_set_completion_flags(thread_idx) later
end
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
2da839f to
85aec90
Compare
85aec90 to
7eb4bda
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp (1)
699-712: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winLower the assigned thread count instead of returning 0 for the all-active case.
nthreads == 1becomesaicpu_thread_num_ = 1, which makesassign_cores_to_threads()returnfalsebecause scheduler threads are configured to be fewer thannthreads. Also avoid assigningaicpu_thread_num_ == MAX_AICPU_THREADS:assign_cores_to_threads()then loops over allcore_trackers_/array entries whileaic_count_ == 0, soaic_count_ / active_sched_threads_yields 0 and no cores are registered (same as returning 0 early withaicpu_thread_num_ = 2).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp` around lines 699 - 712, Update the scheduler-thread count calculation that feeds SchedulerContext::assign_cores_to_threads() so the all-active case lowers the count instead of returning 0: when nthreads == 1, set aicpu_thread_num_ to 1 only if that satisfies the configured constraint, otherwise reduce it to a valid value; never assign MAX_AICPU_THREADS, particularly when aic_count_ == 0. Preserve a positive thread count that lets assign_cores_to_threads() complete without zero-cluster division or empty core registration.
🧹 Nitpick comments (3)
tests/ut/cpp/CMakeLists.txt (1)
670-697: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate boilerplate from
add_a2a3_hbg_runtime_test.Lines 674-697 repeat the include directories, link libraries, and test-registration code from
add_a2a3_hbg_runtime_test(lines 122-147). Only the extra compiled sources differ:pto_shared_memory.cpphere versusscope_stats_collector_aicpu.cppin the function.Generalize the function to accept an extra-sources list. This removes the duplicate block and keeps future host-build-graph test targets consistent.
♻️ Proposed refactor
-function(add_a2a3_hbg_runtime_test name src) +function(add_a2a3_hbg_runtime_test name src) + set(extra_srcs ${ARGN}) add_executable(${name} ${src} ${CMAKE_SOURCE_DIR}/stubs/test_stubs.cpp - ${CMAKE_SOURCE_DIR}/../../../src/common/platform/shared/aicpu/scope_stats_collector_aicpu.cpp + ${extra_srcs} ) ... endfunction()Then define the new test as:
add_a2a3_hbg_runtime_test(test_hbg_shared_memory a2a3/test_hbg_shared_memory.cpp ${CMAKE_SOURCE_DIR}/../../../src/a2a3/runtime/host_build_graph/runtime/shared/pto_shared_memory.cpp )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/ut/cpp/CMakeLists.txt` around lines 670 - 697, Update add_a2a3_hbg_runtime_test to accept and append an extra-sources list when creating the executable, while retaining its existing include directories, link libraries, test registration, and labels. Replace the standalone test_hbg_shared_memory target block with an add_a2a3_hbg_runtime_test call passing its test source and pto_shared_memory.cpp.src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cpp (1)
1064-1079: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCorrect the watermark comment.
The code is right. The explanation is not.
update_completed_watermarkwalks forward withis_completion_flag_set(next), so a later device completer that lands exactly on the frontier does walk past this pre-set flag. The precise reason for the explicit host call is narrower: only a completer whoselocal_idequals the current watermark advances it, so if the frontier already sits at this task's id, no other completer will ever call with that id.📝 Proposed comment fix
- // every consumer register_wakes on a producer that never runs on device and - // the run hangs. update_completed_watermark only advances when called with - // local_id equal to the current watermark, so this task's own call is the - // only chance to move the watermark past it — a later on-device completer - // whose local_id no longer matches the (still-stuck) watermark will no-op, - // not walk past this pre-set flag on our behalf. + // every consumer register_wakes on a producer that never runs on device and + // the run hangs. update_completed_watermark advances only when its local_id + // equals the current watermark. No device thread ever calls it with THIS + // task's local_id, so if the frontier already sits at this id, only this + // call can move it forward. (A later device completer that does land on the + // frontier walks over this pre-set flag as part of its prefix walk.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cpp` around lines 1064 - 1079, Update the multi-line comment block preceding the set_completion_flag and update_completed_watermark calls to correct the explanation of watermark advancement behavior. Replace the incorrect statement that a later device completer will no-op and not walk past the pre-set flag with the accurate explanation that update_completed_watermark walks forward using is_completion_flag_set, so device completers landing on the frontier do walk past pre-set flags. Clarify the narrower and actual reason for the explicit host call: only a completer whose local_id equals the current watermark advances it, so if the frontier already sits at this task's local_id (done_local), no other completer will ever call with that matching id to move the watermark forward.src/a2a3/runtime/host_build_graph/runtime/scheduler/pto_scheduler.h (1)
474-515: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winReplace
reallocwith arena or fixed-storage for the failed-completion heap.
FailedCompletionFlagHeapis allocated from the AICPU scheduler state but still calls stdlibcrealloc/freeand aborts on allocation failure. Since this heap is only needed in rare completion-flag CAS contention, use a small fixed capacity or the scheduler arena instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/pto_scheduler.h` around lines 474 - 515, Update FailedCompletionFlagHeap to avoid stdlibc realloc/free and allocation-failure aborts by using fixed-capacity storage or allocation from the scheduler arena. Preserve push/pop heap behavior and ensure destroy performs only the corresponding non-stdlib cleanup, with capacity sized for the rare completion-flag contention use case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/a2a3/runtime/host_build_graph/runtime/shared/pto_shared_memory.cpp`:
- Around line 153-160: Update the task_window_sizes validation loop in the
shared-memory initialization path to reject zero values before calling
__builtin_ctzll and reject any non-power-of-two value. Keep the existing
shuffle_lower_bits constraint, ensuring every accepted size is a nonzero power
of two with sufficient trailing zero bits before task_window_size is assigned.
---
Outside diff comments:
In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cpp`:
- Around line 699-712: Update the scheduler-thread count calculation that feeds
SchedulerContext::assign_cores_to_threads() so the all-active case lowers the
count instead of returning 0: when nthreads == 1, set aicpu_thread_num_ to 1
only if that satisfies the configured constraint, otherwise reduce it to a valid
value; never assign MAX_AICPU_THREADS, particularly when aic_count_ == 0.
Preserve a positive thread count that lets assign_cores_to_threads() complete
without zero-cluster division or empty core registration.
---
Nitpick comments:
In
`@src/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cpp`:
- Around line 1064-1079: Update the multi-line comment block preceding the
set_completion_flag and update_completed_watermark calls to correct the
explanation of watermark advancement behavior. Replace the incorrect statement
that a later device completer will no-op and not walk past the pre-set flag with
the accurate explanation that update_completed_watermark walks forward using
is_completion_flag_set, so device completers landing on the frontier do walk
past pre-set flags. Clarify the narrower and actual reason for the explicit host
call: only a completer whose local_id equals the current watermark advances it,
so if the frontier already sits at this task's local_id (done_local), no other
completer will ever call with that matching id to move the watermark forward.
In `@src/a2a3/runtime/host_build_graph/runtime/scheduler/pto_scheduler.h`:
- Around line 474-515: Update FailedCompletionFlagHeap to avoid stdlibc
realloc/free and allocation-failure aborts by using fixed-capacity storage or
allocation from the scheduler arena. Preserve push/pop heap behavior and ensure
destroy performs only the corresponding non-stdlib cleanup, with capacity sized
for the rare completion-flag contention use case.
In `@tests/ut/cpp/CMakeLists.txt`:
- Around line 670-697: Update add_a2a3_hbg_runtime_test to accept and append an
extra-sources list when creating the executable, while retaining its existing
include directories, link libraries, test registration, and labels. Replace the
standalone test_hbg_shared_memory target block with an add_a2a3_hbg_runtime_test
call passing its test source and pto_shared_memory.cpp.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 44dad46c-6e3c-47e9-97aa-96c14a2fab67
📒 Files selected for processing (16)
src/a2a3/runtime/host_build_graph/aicpu/aicpu_executor.cppsrc/a2a3/runtime/host_build_graph/docs/RUNTIME_LOGIC.mdsrc/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_orchestrator.cppsrc/a2a3/runtime/host_build_graph/runtime/orchestrator_core/pto_runtime2.cppsrc/a2a3/runtime/host_build_graph/runtime/pto_async_wait.hsrc/a2a3/runtime/host_build_graph/runtime/pto_runtime2_types.hsrc/a2a3/runtime/host_build_graph/runtime/pto_shared_memory.hsrc/a2a3/runtime/host_build_graph/runtime/scheduler/pto_scheduler.hsrc/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_cold_path.cppsrc/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_completion.cppsrc/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_context.hsrc/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_dispatch.cppsrc/a2a3/runtime/host_build_graph/runtime/shared/pto_runtime2_init.cppsrc/a2a3/runtime/host_build_graph/runtime/shared/pto_shared_memory.cpptests/ut/cpp/CMakeLists.txttests/ut/cpp/a2a3/test_hbg_shared_memory.cpp
💤 Files with no reviewable changes (1)
- src/a2a3/runtime/host_build_graph/runtime/scheduler/scheduler_context.h
…ulers Co-authored-by: noabauma <noah.baumann@h-partners.com> Co-authored-by: Sergio Martin <eienburuu@gmail.com>
7eb4bda to
a3a637f
Compare
|
Thanks for putting both variants up side by side — having #1618 and #1619 as an explicit either/or made this much easier to reason about. Two separate topics below: the forward-looking functional groundwork, and the performance case. 1. Functional groundwork (flag-slot reuse) — the premise does not hold in hbgThe hbg's orchestrator and scheduler are strictly serial, not overlapped. By the time any scheduler thread exists, the host has already finished submitting. So even with a complete reclaim protocol in place, the slots freed by reclaim would have no consumer — nobody is waiting to submit into them. Reclaim pays off only when the host wants to submit task N while the window is full, i.e. when O and S overlap in time. That is the Consistent with that, // pto_scheduler.h:26
// reclaim (whole-graph-resident), so last_task_alive is not advanced here.which pins the ring allocator's gate at (It runs fine with One more thing worth noting for whenever reclaim is taken up: So as things stand, none of this machinery is exercised on hbg: the reuse gate never fails, the failsafe heap stays empty, and 2. Performance — we measure this PR at parity with its baseWe benchmarked the PR head against its own merge-base on one card. Results (a2a3,
|
| workload | base (merge-base) | #1618 | Δ |
|---|---|---|---|
qwen3_14b_decode StressBatch16Seq3500 |
36.550 ms | 36.331 ms | −0.60% |
paged_attention Case1 |
24.816 ms | 25.328 ms | +2.06% (within noise) |
bgemm / matmul / vector_example were also run, but at 0.10–0.19 ms their measured cv is 5–12%, so they carry no resolution at this scale and we are not quoting them.
Method
- Baseline = this PR's merge-base
80aa2876; current =a3a637f0. The only difference between the two trees is this PR's single commit. - Two detached-HEAD worktrees, each with its own venv and
pip install --no-build-isolation -e ., so the nanobind extension and the runtime binaries are fully isolated per side. - Same physical card: the whole comparison runs inside a single
task-submitinvocation, so one device lock covers both sides; baseline and PR run back-to-back per workload. - 100 rounds per side, round 0 dropped as warm-up, outliers trimmed, then the mean.
- Metric is the
runner_run.device_wallspan from the[STRACE]markers, rendered bypython -m simpler_setup.tools.strace_timing <log> --rounds-table. - On pa, round times fall into two groups (~24 ms and ~51 ms, roughly half each, on both sides); the figure quoted above is the fast group. We are looking into the cause separately.
paged_attentionCase1 needsPTO2_RING_TASK_WINDOW=131072 PTO2_RING_HEAP=805306368to run at all (see §1).qwen3_14b_decodeships asruntime="tensormap_and_ringbuffer"; we re-pointed it to"host_build_graph"— a one-line change, verified byte-identical in both worktrees.
Cross-check against #1544
Our base (which contains 3S+1P) measures 24.816 ms on pa Case1. The 3S+1P number posted in #1544 is 25.013 ms — 0.8% apart, from an independent measurement on a different device on a different day.
Taking #1544's 4S baseline (28.016 ms) as the reference for a plain revert:
plain 4S revert (no optimisations) 28.016 ms [#1544]
#1618 (4S + shuffle + frontier gate) 25.328 ms [this measurement]
3S+1P base 24.816 ms [this measurement]
So the bit-shuffle and the frontier-gated watermark clearly do real work — they recover roughly 9.6 of the 10.7 points a plain revert would cost (that comparison is cross-experiment — different day, device, and 4S baseline build — so treat the figure as indicative). But against the current base they land at parity, not ahead.
What we would like to reconcile
Our numbers do not line up with the improvement the charts in the PR body appear to show, and we cannot read the underlying values off the images. Could you post the numbers as text, along with your measurement method and what the baseline is — main, or #1619? Since both PRs revert 3S+1P, a #1618-vs-#1619 delta measures only eager-vs-lazy watermark maintenance, which is a much narrower claim than #1618-vs-main.
3. 3S+1P is not only about ready-queue contention
One consideration behind the 3S+1P split that we would like to see represented in the comparison: the dedicated P thread does more than remove multi-producer contention. It also takes async deferred-completion polling and dummy / predicate-failed retirement off the dispatch path entirely. From main's own comment, which this PR removes:
Async deferred-completion polling and dependency-only (dummy / predicate-failed) retirement both run on P, which owns every completion→ready transition — the scheduler threads' loop stays purely core-local (poll own COND, dispatch own cores) and never touches the shared mailbox or dummy queue.
and, for the dummy queue specifically:
P produces and drains it, so the queue is single-threaded end to end.
Under 4S both come back onto every scheduler thread's loop: async_wait_list.poll_and_complete behind a shared try_lock(), and dummy_ready_queue becomes a contended MPMC queue drained by all four threads. For graphs with substantial async or dummy/barrier traffic, that is work now sitting on the dispatch path of every thread rather than on a thread that owns no cores. We expect 3S+1P to hold a substantial advantage on those workloads that 4S cannot match, and that class is where the two designs should really be separated.
To be clear on where we stand: §1 and §3 are observations on the design, and §2 is a request for data rather than an objection. If the numbers show #1618 ahead of main under a method we can reproduce, that settles it.
1. Functional groundwork (flag-slot reuse) — the premise does not hold in hbgThere are two options here:
In the first case, there is no need for the mask in In the second case, reuse of flags is necessary. This also removes the necessity to increase If the first case is truely the design choice, then a lot of the PR can be stripped. |
3. 3S+1P is not only about ready-queue contentionI believe it all comes down to a proper benchmark for this case.
I suggest the different designs |
|
Thanks — that is exactly the right way to frame it, and the answer is case 1. Case 1 is not a preference, it is enforced todayThe ring allocator gate is Why the code reads ambiguously here
The result is that several ring concepts survive in hbg without carrying any meaning:
These vestiges are what make the design intent ambiguous when reading the code, and removing them is precisely what we want to do next. So your instinct that a lot can be stripped is right — and it reaches beyond the code this PR touches. It is also the direction, not just the status quoThe concern behind case 2 is presumably that a large model submits too many tasks — a 40-layer Qwen decode being the obvious example. Our answer to that is not a bigger flag array but a smaller task count: #1444 adds a So the intent is to keep task counts comfortably below the flag array, not to make the array circular. On the two things you would stripThe mask — agreed, under case 1 The watermark — you are right to question it, and it holds up on inspection. Once reuse is out, its only reader is So it performs no real gating today. The one thing worth deciding deliberately rather than as a side effect: removing it also removes One correction on case 2
The parenthetical is doing a lot of work there. |
|
Agreed — a proper benchmark is the right next step, and it is what we intend to focus on next. A shared benchmark is also what we need for performance alignment generally, not just for this decision. Where the thread budget comes fromFraming this as "3 vs 4 scheduling threads" understates what actually changed. The AICPU thread count is an architecture-level choice made per platform, for hardware reasons we are happy to go into separately:
So the number of core-owning threads has been 3 throughout on a2a3. What #1618 does is not "3 → 4 schedulers"; it is spending the thread freed by moving O to the host on a fourth core-owner rather than on a resolver. The real question is therefore how best to spend that freed thread, and that is exactly what a benchmark should answer. On
|
|
@noabauma — flagging you since you own the performance testing. Thanks for the charts — having the ladder made this straightforward to reproduce. We ran the same ladder against this PR's merge-base and cannot reproduce the improvement: we measure the two branches at parity on every rung. Our results — device_wall, base (80aa287) vs #1618 (a3a637f)
Every rung is within ±0.3%, against cv of 1.0–9.5%. Method
Side by side
Our Where your figures fall inside our distributions:
What we think could produce this1. The runs may not have been interleaved. We ran 2. Card selection. The chart says 3. A genuine machine difference — but the pattern argues against it. A machine or environment difference should shift both branches the same way. It does not. Your Could you describe your setup in more detail?Two notes on our side first, since both are places where we may already have diverged from you:
Beyond that: to work out where the difference comes from, could you describe your measurement in detail — the environment and how the runs were organised (branch builds, CANN version, which device(s) and whether they were held exclusively, and how the three branches were ordered relative to one another) — and the data collection point: which markers or which tool produce the "whole-device wall clock" and the "AICore task execution time" spans. With that we can align our runs to yours and narrow down what produces the gap, rather than continuing to measure separately. Happy to re-run anything under a configuration you specify. |
Refactor: host build graph, eager completion watermark, 4 AICPU schedulers
Base:
main· Branch:refactor/eager-completion-mark· 1 commit, 15 files (+651/-466)One of two possible versions of
host-build-graphcompletion-watermark refactor the other one being #1619.This eager one is more performant due to minimal work, but has delicate synchronization logic.
Summary
Reverts the 3S+1P scheduler split back to 4 uniform AICPU schedulers, and reworks
completed_watermarkmaintenance to be eager rather than lazy. Eager updates requirecompletion_flagsto become a reusable, thread-safe structure (previously a singlebyte per slot, implicitly host-only), which in turn needed a failsafe against the
flag-slot-reuse deadlock the old design couldn't hit.
1. Revert: 3 schedulers + 1 dedicated resolution thread → 4 AICPU schedulers
The earlier design split AICPU threads into 3 core-owning scheduler (S) threads plus
1 core-less resolution (P) thread that alone drained completions, published
completion_flags, drained wake lists, and advanced the watermark — funneling allcompletion resolution through a single thread.
This PR removes that split entirely:
CompletedTaskQueue(the per-S → P SPSC handoff ring) andrun_resolution_threadare deleted (
scheduler_context.h,scheduler_dispatch.cpp).p_thread_idx()/p_thread_idx_are gone;assign_cores_to_threadsno longerreserves the last thread as core-less, and the
aicpu_thread_num >= 2floor (1 S +1 P) is dropped —
active_sched_threads_ = aicpu_thread_num_again, so all 4threads own cores and resolve their own completions.
aicpu_executor.cppcallsresolve_and_dispatchuniformly instead of branching onwhether a thread is the P thread.
job) move back into each scheduler thread's own
resolve_and_dispatchloop.2. Completion watermark: eager updates
completed_watermarkis now advanced eagerly by every completer, not justopportunistically:
on_mixed_task_complete), its deferred retry(
retry_set_completion_flags), and the host orchestrator's inline hidden-alloccompletion — calls
update_completed_watermark(thread_idx, my_id)exactly once,immediately after that id's own
completion_flagsentry is actually visible.my_idis exactly the current watermark frontier; onlythe completer landing at the frontier does the CAS-advance walk over the full
contiguous completed prefix. Out-of-order completers defer to whoever completes the
frontier task later.
cached_completed_watermarkavoids re-reading the atomic on everyis_completion_flag_setcheck by falling back to a cached watermark value.completed_watermarkis now "lowest id not yetguaranteed complete" (was "highest id guaranteed complete"), so comparisons flip
from
>=to>at call sites (wait_for_tensor_ready, reclaim gates, etc.).3.
completion_flags: fewer flags than tasks, slots reusedcompletion_flagschanges from auint8_t[task_window_size]byte array (host-onlywriter, implicitly one-shot) to an
int32_t[task_window_size]array where each entrystores either
-1(pending) or thelocal_idthat owns it. Because the stored valueis the id itself rather than a boolean, a slot can be safely reused across laps:
the array no longer needs to be sized to the total task count, only to the ring's
task window —
local_idandlocal_id + task_window_sizeshare a slot, and reuse isgated on
completed_watermarkhaving certified the slot's previous occupant first.flag_index()bit-reindexeslocal_id & task_window_mask(swaps lowshuffle_lower_bitsbits into the high position) so consecutive ids land ondifferent cachelines, keeping
update_completed_watermark's linear scancache-friendly.
set_completion_flag(host, blocking) and the newtry_set_completion_flag(device, non-blocking) both gate the store on the previous occupant being
certified;
try_set_completion_flagreturnsfalseinstead of spinning when itisn't.
is_completion_flag_setfalls back tocompleted_watermarkso a slot that's beenoverwritten by a later lap still reports the earlier id as complete.
4. Deadlock failsafe for flag-slot reuse
The correctness argument for reuse is: task
tmust not depend on a task with id>= t + task_window_size, which holds automatically since task ids follow thedependency graph's topological order. But rather than assume that invariant always
holds, a failsafe absorbs a violation instead of deadlocking:
try_set_completion_flagfails insideon_mixed_task_complete, the task id ispushed onto a per-thread min-heap (
failed_heap_of_set_completion_flag) instead ofthe thread spinning or blocking. Wake-list drain and the watermark update for that
id are skipped and deferred.
retry_set_completion_flags, which retries thesmallest pending id in the heap; on success it drains that task's wake list and
advances the watermark — its one deferred chance, taken later instead of never.
isn't — it exists purely as a backstop, not a normal-path mechanism.
Also in this diff
docs/RUNTIME_LOGIC.md(§6.2, §7.2, §8.2, §8.4) rewritten to match the above.test_hbg_shared_memory.cppcovers theshuffle_higher_bitsinvariant
flag_index()depends on (rejects atask_window_sizetoo small toprovide
shuffle_lower_bitsof headroom, which would otherwise be a negativeshift / UB).
PTO2SharedMemoryRingHeadergrows from 256 → 576 bytes andPTO2SharedMemoryHeaderfrom 320 → 640 bytes (newcached_completed_watermarkarray + wider
completion_flagsentries); layoutstatic_asserts updatedaccordingly.
Performance
Comparison against main and #1619 for both device wall-clock and kernel only (as measured by tracr)